// ==================================================================================================
// Copyright(C) 2018 FI Ltd. All rights reserved.
//
// SYSTEM NAME	   : FI.Net Application Framework 
// COMPONENT ID	   : FI.Helpers.Data.DataSetHelper
// COMPONENT DESC  : Utility class for DataSet	
//
// CREATED DATE/BY : 18 Aug 2018 / Tian Mi
//
// REVISION HISTORY:	DATE/BY			SR#/CS/PM#/OTHERS		DESCRIPTION OF CHANGE
// ==================================================================================================

using System;
using System.Collections.Generic;
using System.Data;
using System.Linq;
using System.Threading;
using FI.Helpers.Data.Properties;
using FI.Helpers.Data.Configuration;

namespace FI.Helpers.Data
{
    /// <summary>
    /// Helper class for dataSet.
    /// </summary>
    internal static class DataSetHelper
    {

        /// <summary>
        /// Updates the System Fields like versio no field, created by field, etc.
        /// </summary>
        /// <param name="dataRows">Input DataRow Array.</param>
        /// <param name="transactionId">The current transaction id.</param>
        /// <param name="userId">The current user id.</param>
        /// <param name="dataAccessSettings">The data access settings.</param>
        public static void UpdateSystemField(DataRow[] dataRows, string transactionId, string userId, DataAccessSettings dataAccessSettings = null)
        {
            if (dataRows.Length == 0)
            {
                return;
            }

            dataRows[0].Table.BeginLoadData();

            foreach (DataRow dataRow in dataRows)
            {
                bool isDeleteRow = false;

                if (dataRow.RowState == DataRowState.Deleted)
                {
                    isDeleteRow = true;
                    dataRow.RejectChanges();
                }

                if (!string.IsNullOrEmpty(DataAccessConfigurationView.GetVersionNoField(dataAccessSettings))
                    && dataRow.Table.Columns.Contains(DataAccessConfigurationView.GetVersionNoField(dataAccessSettings)))
                {
                    if (dataRow.RowState == DataRowState.Modified)
                    {
                        dataRow[DataAccessConfigurationView.GetVersionNoField(dataAccessSettings)] =
                                int.Parse(dataRow[DataAccessConfigurationView.GetVersionNoField(dataAccessSettings)].ToString()) + 1;
                    }
                    else if (dataRow.RowState == DataRowState.Added)
                    {
                        dataRow[DataAccessConfigurationView.GetVersionNoField(dataAccessSettings)] = 1;
                    }
                }

                if (!string.IsNullOrEmpty(DataAccessConfigurationView.GetTransactionIdField(dataAccessSettings))
                    && dataRow.Table.Columns.Contains(DataAccessConfigurationView.GetTransactionIdField(dataAccessSettings)))
                {
                    dataRow[DataAccessConfigurationView.GetTransactionIdField(dataAccessSettings)] = transactionId;
                }

                if (dataRow.RowState == DataRowState.Added)
                {
                    if (!string.IsNullOrEmpty(DataAccessConfigurationView.GetCreatedByField(dataAccessSettings))
                        && dataRow.Table.Columns.Contains(DataAccessConfigurationView.GetCreatedByField(dataAccessSettings)))
                    {
                        dataRow[DataAccessConfigurationView.GetCreatedByField(dataAccessSettings)] = userId;
                    }
                    if (!string.IsNullOrEmpty(DataAccessConfigurationView.GetCreatedTimeField(dataAccessSettings))
                        && dataRow.Table.Columns.Contains(DataAccessConfigurationView.GetCreatedTimeField(dataAccessSettings)))
                    {
                        dataRow[DataAccessConfigurationView.GetCreatedTimeField(dataAccessSettings)] = DateTime.Now;
                    }
                }

                if (!string.IsNullOrEmpty(DataAccessConfigurationView.GetLastUpdatedByField(dataAccessSettings))
                    && dataRow.Table.Columns.Contains(DataAccessConfigurationView.GetLastUpdatedByField(dataAccessSettings)))
                {
                    dataRow[DataAccessConfigurationView.GetLastUpdatedByField(dataAccessSettings)] = userId;
                }

                if (!string.IsNullOrEmpty(DataAccessConfigurationView.GetLastUpdatedTimeField(dataAccessSettings))
                    && dataRow.Table.Columns.Contains(DataAccessConfigurationView.GetLastUpdatedTimeField(dataAccessSettings)))
                {
                    dataRow[DataAccessConfigurationView.GetLastUpdatedTimeField(dataAccessSettings)] = DateTime.Now;
                }

                if (isDeleteRow)
                {
                    dataRow.AcceptChanges();
                    //Accept changes for delete records, provide transactionId for audit log module.
                    dataRow.Delete();
                }
            }

            dataRows[0].Table.EndLoadData();
        }

        #region Get table order in dataset.

        /// <summary>
        /// Gets the data table order.
        /// </summary>
        /// <param name="targetDS">The target DS.</param>
        /// <returns>A string array containsing the ordered table name.</returns>
        public static string[] GetDataTableOrder(DataSet targetDS)
        {
            return GetDataTableOrder(targetDS, true);
        }

        /// <summary>
        /// Gets the data table order.
        /// </summary>
        /// <param name="ds">The target DS.</param>
        /// <param name="masterToChild">if set to <c>true</c> [master to child].</param>
        /// <returns>A string array containsing the ordered table name.</returns>
        public static string[] GetDataTableOrder(DataSet ds, bool masterToChild)
        {
            // Create original DataSet instance without any change.
            DataSet targetDS = (DataSet)Activator.CreateInstance(ds.GetType());

            IList<string> sortedTables = new List<string>();
            List<DataRelation> handledRelations = new List<DataRelation>();

            // Section I: sort tables on target DataSet with relations.
            foreach (DataRelation relation in targetDS.Relations)
            {
                string parentTable = relation.ParentTable.TableName;
                bool isNewParent = !sortedTables.Contains(parentTable);

                string childTable = relation.ChildTable.TableName;
                bool isNewChild = !sortedTables.Contains(childTable);

                // Handling parent element.
                if (isNewParent) // Case 1: Parent is new.
                {
                    sortedTables.Add(parentTable);
                }

                // Handling child element.
                if (isNewChild) // Case 3, 4: Child is new despite of any parent.
                {
                    sortedTables.Add(childTable);
                }
                else if (!isNewChild && isNewParent) // Case 5: Child is existed and parent is new.
                {
                    // Only insert parent element before existed child element.
                    InsertBefore(parentTable, childTable, ref sortedTables);
                }
                else if (!isNewChild
                    && !isNewParent
                    && sortedTables.IndexOf(parentTable) > sortedTables.IndexOf(childTable)) // Case 6: Child and parent is existed, and just index of child is more than parents'.
                {
                    // TODO: maybe throw exception because of unexpected table relations.
                    int parentIndex = sortedTables.IndexOf(parentTable);
                    int childIndex = sortedTables.IndexOf(childTable);
                    IList<string> tableCheckList = new List<string>();
                    for (int index = childIndex; index < parentIndex; index++)
                    {
                        tableCheckList.Add(sortedTables[index]);
                    }

                    // First step:
                    InsertBefore(parentTable, childTable, ref sortedTables);

                    // Next steps:
                    // Check elements between target parent and child element.
                    var parentsOnParentTable = handledRelations
                        .Where(t => t.ChildTable.TableName.Equals(parentTable))
                        .Select(t => t.ParentTable.TableName);
                    IList<string> changableTables = new List<string>();
                    if (parentsOnParentTable.Count() > 0)
                    {
                        changableTables = tableCheckList
                            .Where(t => parentsOnParentTable.Contains(t))
                            .ToList();
                    }

                    if (changableTables.Count > 0)
                    {
                        int checkedStartIndex = changableTables.Count - 1;
                        for (int index = checkedStartIndex; index > -1; index--)
                        {
                            string checkTable = changableTables[index];

                            #region Validate checked table (DATASET NOT SUPPORTED)
                            IList<string> validatingTableCheckList = new List<string>();
                            int indexInTableCheckList = tableCheckList.IndexOf(checkTable);
                            for (int validateIndex = 0; validateIndex < indexInTableCheckList; validateIndex++)
                            {
                                validatingTableCheckList.Add(tableCheckList[validateIndex]);
                            }
                            var validatedTableCheckList = validatingTableCheckList
                                .Where(t => !changableTables.Contains(t));
                            var validatedTableList = handledRelations
                                .Where(r => r.ChildTable.TableName.Equals(checkTable))
                                .Select(r => r.ParentTable.TableName);
                            if (validatedTableCheckList
                                .Where(t => validatedTableList.Contains(t))
                                .Count()
                                > 0)
                                throw new ApplicationException("The DataSet cannot be supported to calculate table order.");

                            #endregion

                            if (index == checkedStartIndex)
                                InsertBefore(checkTable, parentTable, ref sortedTables);
                            else
                                InsertBefore(checkTable, changableTables[index + 1], ref sortedTables); ;
                        }
                    }
                }

                handledRelations.Add(relation);
            }

            // Section II: append tables with no relations in sorted table list.
            foreach (DataTable oTable in targetDS.Tables)
            {
                if (!sortedTables.Contains(oTable.TableName))
                {
                    sortedTables.Add(oTable.TableName);
                }
            }

            return masterToChild
                ? sortedTables.ToArray()
                : sortedTables.Reverse().ToArray();
        }

        /// <summary>
        /// Inserts the before.
        /// </summary>
        /// <param name="sourceElement">The source element.</param>
        /// <param name="targetElement">The target element.</param>
        /// <param name="sortedTables">The sorted tables.</param>
        private static void InsertBefore(string sourceElement, string targetElement, ref IList<string> sortedTables)
        {
            int sourceIndex = sortedTables.IndexOf(sourceElement);
            int targetIndex = sortedTables.IndexOf(targetElement);
            for (int index = sourceIndex; index > targetIndex; index--)
            {
                sortedTables[index] = sortedTables[index - 1];
            }
            sortedTables[targetIndex] = sourceElement;
        }

        #endregion

        /// <summary>
        /// Checks the datatable.
        /// </summary>
        /// <param name="dataTable">The input daatable which need to be checked.</param>
        /// <exception cref="Exception">When the table doesn't have primary key.</exception>
        public static void CheckDataTable(DataTable dataTable)
        {
            if (dataTable.PrimaryKey.Length == 0)
            {
                throw new PrimaryKeyNotFoundException(Resources.HaveNotPK);
            }
        }

        /// <summary>
        /// Checks the dataset.
        /// </summary>
        /// <param name="dataSet">The input dataset which need to be checked.</param>
        /// <exception cref="Exception">When there are some tables which don't have relationship.</exception>        
        public static void CheckDataSet(DataSet dataSet)
        {
            List<string> tableList = new List<string>();
            if (dataSet.Tables.Count > 1)
            {
                for (int i = 0; i < dataSet.Tables.Count; i++)
                {
                    tableList.Add(dataSet.Tables[i].TableName);
                }

                foreach (DataRelation dataRelation in dataSet.Relations)
                {
                    tableList.Remove(dataRelation.ParentTable.TableName);
                    tableList.Remove(dataRelation.ChildTable.TableName);
                }
                //The method forced all tables in the same dataset must have relationship. It was decided when to do TCMS project. History issue. 
                //Its objective was to force developers follow up the guide.
                //But for FI.Net, it was not nessary. However, it was released long long ago. So it remained here.
                if (tableList.Count > 0)
                {
                    throw new OrphanTableFoundException(Resources.HaveIndependentTable);
                }
            }
            foreach (DataTable dataTable in dataSet.Tables)
            {
                CheckDataTable(dataTable);
            }
        }
    }
}